#!/usr/bin/env node

require('dotenv').config();
process.on("uncaughtException", (e) => {
  console.error(e);
});

/**
 * Module dependencies.
 */

const app = require('./app');
const debug = require('debug')('cyue-webrtc:server');
const path = require("node:path");
const fs = require("node:fs");
const http = require('http');
const https = require('https');
const WebSocketServer = require("websocket").server;
const userData = require('./user-data');
const db = require('better-sqlite3')(process.env.DATABASE_PATH || "./database.db");


/**
 * Get port from environment and store in Express.
 */

const port = normalizePort(process.env.PORT || '5555');
app.set('port', port);

/**
 * Create HTTP(S) server.
 */

const server = (function(){
  console.log(process.env.NODE_ENV);
  if (
    process.env.SSL_KEY_FILE_PATH &&
    process.env.SSL_CERT_FILE_PATH
  ) {
    const certPath = path.resolve(__dirname, process.env.SSL_CERT_FILE_PATH);
    const keyPath = path.resolve(__dirname, process.env.SSL_KEY_FILE_PATH);
    console.info("加载证书文件：", certPath);
    console.info("加载密钥文件：", keyPath);
    return https.createServer({
      cert: fs.readFileSync(certPath),
      key: fs.readFileSync(keyPath),
    }, app);
  }
  return http.createServer(app);  
}());

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  const port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  const bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  const addr = server.address();
  const bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}


const DATA_DESTATION = {
  CLIENT: "Client",
  SERVER: "Server",
  ALL: "all",
};
const MESSAGE_TABLE_NAME = "public_message";

class Command {
  static TYPE_LOGIN = 'login';
  static TYPE_TEXT_PUBLIC = 'text_public';
  static TYPE_TEXT = 'text';
  static TYPE_OFFER = 'offer';
  static TYPE_ANSWER = 'answer';
  static TYPE_CANDIDATE = 'candidate';

  constructor(text) {
    try {
      const cmd = JSON.parse(text);
      this.type = cmd.type;
      this.nickname = cmd.nickname;
      this.to = cmd.to; // UserID
      this.data = cmd.data;
    } catch (e) {
      console.error(e);
    }
  }
}
class Message {
  static TYPE_LOGIN_SUCCESS = 'login_success';
  static TYPE_ADDRESS = 'address';
  static TYPE_TEXT_PUBLIC = 'text_public';
  static TYPE_TEXT = 'text';
  static TYPE_OFFER = 'offer';
  static TYPE_ANSWER = 'answer';
  static TYPE_CANDIDATE = 'candidate';
  constructor(type, nickname, from, data) {
      this.type = type;
      this.nickname = nickname;
      this.from = from; // UserID
      this.time = new Date();
      this.data = data;
  }
}


function originIsAllowed(origin) {
  return (
    process.env.ALLOWED_ORIGINS === "*" ||
    process.env.ALLOWED_ORIGINS.split(",").reduce((allowed, item) => {
      return allowed || origin.includes(item.trim());
    }, false)
  );
}


// WebSocket server setup
const wsServer = new WebSocketServer({
  httpServer: server,
  // You should not use autoAcceptConnections for production
  // applications, as it defeats all standard cross-origin protection
  // facilities built into the protocol and the browser.  You should
  // *always* verify the connection's origin and decide whether or not
  // to accept it.
  autoAcceptConnections: false
});


wsServer.on('request', function (request) {
  let user;
  let address = request.remoteAddress || request.socket.remoteAddress;
  // 127.0.0.1 可能是代理
  if (!request.socket.remoteAddress.includes('127.0.0.1')) {
    address += ":" + request.socket.remotePort;
  } else if (request.httpRequest.headers["x-forwarded-port"]) {
    // 反向代理时获取客户端端口
    address += ":" + request.httpRequest.headers["x-forwarded-port"].trim();
  }
  console.info(new Date(), address, '请求连接');
  if (!originIsAllowed(request.origin)) {
    // Make sure we only accept requests from an allowed origin
    request.reject();
    console.info(`\x1B[31m ${new Date()} 来自 ${request.origin} 的连接被拒绝。\x1B[0m`);
    return;
  }

  const connection = request.accept("cyue-webrtc", request.origin);
  const addressMessage = new Message(Message.TYPE_ADDRESS, undefined, DATA_DESTATION.SERVER, address);
  connection.send(JSON.stringify(addressMessage));

  // 发送存储的公共消息
  Promise
  .resolve()
  .then(() => {
    const statement = db.prepare(`SELECT * FROM ${MESSAGE_TABLE_NAME} ORDER BY time DESC LIMIT 1024`).all();
    const messages = new Array(statement.length);
    let startIndex = statement.length - 1;
    for (const row of statement) {
      messages[startIndex--] = {
        type: Message.TYPE_TEXT_PUBLIC,
        ...row
      };
    }
    console.info(`发送 ${messages.length} 条历史消息`);
    for (const m of messages) {
      connection.send(JSON.stringify(m));
    }
  });

  connection.on('message', function (message) {
    if (message.type !== 'utf8') {
      console.log(`错误的消息类型: ${message.type}`);
      return;
    }
    console.info(`收到消息: ${message.utf8Data}`);
    if (message.utf8Data.toLowerCase() === "ping") {
      connection.send("pong");
      return;
    }

    const command = new Command(message.utf8Data);
    if (!command) {
      console.log(`错误的命令`);
      return;
    }
    console.info(`${new Date()} 收到来自 ${command.nickname}（${address}） 的命令: ${command.type} ，目标为 ${command.to || DATA_DESTATION.SERVER}`);
    // 登录
    if (command.type === Command.TYPE_LOGIN) {
      console.log(`用户 ${command.nickname}（${address}） 登录`);
      const nickname = command.nickname || command.data || address;
      user = new userData.UserData(nickname, address, connection);
      userData.addLivingUser(user);
      const loginMessage = new Message(Message.TYPE_LOGIN_SUCCESS, undefined, DATA_DESTATION.SERVER, "登录成功");
      connection.send(JSON.stringify(loginMessage));
      return;
    }

    // 公共消息
    if (command.type === Command.TYPE_TEXT_PUBLIC) {
      const publicMessage = new Message(Message.TYPE_TEXT_PUBLIC, command.nickname, user.getID(), command.data);

      const result = db.prepare(`INSERT INTO ${MESSAGE_TABLE_NAME} (\`from\`, \`nickname\`, \`time\`, \`data\`) VALUES (?, ?, ?, ?)`)
        .run(publicMessage.from, publicMessage.nickname, publicMessage.time.toISOString(), publicMessage.data);
      if (result.changes > 0) {
        console.info(`公共消息存储成功: ${publicMessage.data}`);
      }

      for (const u of userData.getLivingUsers(true)) {
        const c = u.connection;
        if (!c) {
          console.warn(`用户 ${u.nickname}（${u.address}） 的连接已断开`);
          continue;
        }
        c.send(JSON.stringify(publicMessage));
      }
      return;
    }

    // to self
    if (command.to === user.getID()) {
      return;
    }
    // to others
    const toConnection = userData.getLivingUserMap().get(command.to).connection;
    if (!toConnection) {
      console.log('destination connection not found');
      return;
    }
    const replyMessage = new Message(command.type, command.nickname, user.getID(), command.data);
    const replayMessageStr = JSON.stringify(replyMessage);
    if (command.type === Command.TYPE_TEXT) {
      connection.send(replayMessageStr); // to self for finishing the chat
    }
    toConnection.send(replayMessageStr);
  });

  connection.on('close', function (_reasonCode, _description) {
    console.info(`${new Date()} ${user ? user.nickname + "（" + user.address + "）" : address} 断开连接.`);
    if (user) {
      userData.removeLivingUser(user);
    }
  });
});

